program Quake3MapShader;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  SampleCommon,
  DIrr in '..\..\DSource\DIrr.pas',
  DIrr.GUI in '..\..\DSource\DIrr.GUI.pas',
  DIrr.Color in '..\..\DSource\DIrr.Color.pas',
  DIrr.Rect in '..\..\DSource\DIrr.Rect.pas',
  DIrr.Event in '..\..\DSource\DIrr.Event.pas',
  DIrr.Vector2D in '..\..\DSource\DIrr.Vector2D.pas',
  DIrr.Vector3D in '..\..\DSource\DIrr.Vector3D.pas',
  DIrr.Video in '..\..\DSource\DIrr.Video.pas',
  DIrr.Base in '..\..\DSource\DIrr.Base.pas',
  DIrr.Types in '..\..\DSource\DIrr.Types.pas',
  DIrr.Scene in '..\..\DSource\DIrr.Scene.pas',
  DIrr.Structs in '..\..\DSource\DIrr.Structs.pas',
  DIrr.Aabbox3D in '..\..\DSource\DIrr.Aabbox3D.pas',
  DIrr.Matrix4 in '..\..\DSource\DIrr.Matrix4.pas',
  DIrr.Texture in '..\..\DSource\DIrr.Texture.pas',
  DIrr.Dimension2d in '..\..\DSource\DIrr.Dimension2D.pas',
  DIrr.Collections in '..\..\DSource\DIrr.Collections.pas',
  DIrr.ConcreteMesh in '..\..\DSource\DIrr.ConcreteMesh.pas',
  DIrr.Quake3 in '..\..\DSource\DIrr.Quake3.pas',
  DIrr.Buffer in '..\..\DSource\DIrr.Buffer.pas',
  DIrr.IO in '..\..\DSource\DIrr.IO.pas',
  DIrr.Consts in '..\..\DSource\DIrr.Consts.pas';

(*
	define which Quake3 Level should be loaded
*)
{$DEFINE IRRLICHT_QUAKE3_ARENA}
{.$DEFINE ORIGINAL_QUAKE3_ARENA}
{.$DEFINE CUSTOM_QUAKE3_ARENA}
{.$DEFINE SHOW_SHADER_NAME}

{$IFDEF ORIGINAL_QUAKE3_ARENA}
const
  QUAKE3_STORAGE_1: String = '/baseq3/';
  {$IFDEF CUSTOM_QUAKE3_ARENA}
    QUAKE3_STORAGE_2: String = '/cf/';
    QUAKE3_MAP_NAME: String = 'maps/cf.bsp';
  {$ELSE}
    QUAKE3_MAP_NAME: String = 'maps/q3dm8.bsp';
  {$ENDIF}
{$ENDIF}

{$IFDEF IRRLICHT_QUAKE3_ARENA}
const
  QUAKE3_STORAGE_1: String = '../../media/map-20kdm2.pk3';
  QUAKE3_MAP_NAME: String = 'maps/20kdm2.bsp';
{$ENDIF}

(*
A class to produce a series of screenshots
*)

type
  TScreenShotFactory = class(TEventReceiver)
  private
    FDevice: TIrrlichtDevice;
    FNumber: U32;
    FFileNameTemplate: String;
    FNode: TSceneNode;
  public
    constructor Create(Device: TIrrlichtDevice; TemplateName: String;
      Node: TSceneNode);
    function Drop: Boolean; override;
  public
    function OnEvent(const Event: SEvent): Boolean; override;
  end;


{ TScreenShotFactory }

constructor TScreenShotFactory.Create(Device: TIrrlichtDevice;
  TemplateName: String; Node: TSceneNode);
begin
  FDevice := Device;
  FFileNameTemplate := StringReplace(TemplateName, '/', '_', [rfReplaceAll]);
  FFileNameTemplate := StringReplace(FFileNameTemplate, '\', '_', [rfReplaceAll]);

  FNode := Node;
end;

function TScreenShotFactory.Drop: Boolean;
begin
  Result := inherited Drop;
end;

function TScreenShotFactory.OnEvent(const Event: SEvent): Boolean;
var
  Image: TImage;
  Str: String;
begin
  // check if user presses the key F9
  if (Event.EventType = EET_KEY_INPUT_EVENT) and (Event.KeyInput.PressedDown) then
  begin
    if Event.KeyInput.Key = KEY_F9 then
    begin
      Image := FDevice.GetVideoDriver.CreateScreenShot;
      if Assigned(Image) then
      begin
        Inc(FNumber);
        Str := Format('%s_shot%0.4d.jpg', [FFileNameTemplate, FNumber]);
        FDevice.GetVideoDriver.WriteImageToFile(Image, Str, 85);
        Image.Drop;
      end;
    end
    else
    if Event.KeyInput.Key = KEY_F8 then
    begin
      if FNode.IsDebugDataVisible then
        FNode.SetDebugDataVisible(EDS_OFF)
      else
        FNode.SetDebugDataVisible(EDS_BBOX_ALL);
    end;

    Result := False;
  end;

end;

//================================================================

{
Ok, lets start.
}

var
  VideoDim: TDimension2DU;
  MapName: String;
  FileArchive: AnsiString;
  Driver: TVideoDriver;
  Smgr: TSceneManager;
  Gui: TGUIEnvironment;
  DriverType: E_DRIVER_TYPE;
  Device: TIrrlichtDevice;
  Mesh: TQ3LevelMesh;
  Node: TSceneNode;
  Geometry, AdditionalMesh: TMesh;
{$IFDEF SHOW_SHADER_NAME}
  Font: TGUIFont;
  Count: U32;
  ANodeName: WideString;
{$ENDIF}
  i: Integer;
  MeshBuffer: TMeshBuffer;
  MaterialPtr: PMaterial;
  ShaderIndex: S32;
  ShaderPtr: PShader;
  Camera: TCameraSceneNode;
  EntityList: TEntityArray;
  Search: TEntity;
  EntityPtr: PEntity;
  Index, NotEndList: S32;
  GroupPtr: PVarGroup;

  screenshotFactory: TScreenShotFactory;
  ParsePos: U32;
  VPos, VTarget: TVector3DF;
  VAngle: F32;
  ContextStr: AnsiString;
  Str: String;
  Pos2DI: TVector2D;
  LastFPS: Integer = -1;
  FPS: Integer;
  Attr: TAttributes;
begin
  try
    ReportMemoryLeaksOnShutdown := True;

    (*
    Like in the HelloWorld example, we create an IrrlichtDevice with
    createDevice(). The difference now is that we ask the user to select
    which hardware accelerated driver to use. The Software device would be
    too slow to draw a huge Quake 3 map, but just for the fun of it, we make
    this decision possible too.
    *)

    // ask user for driver
    DriverType := DriverChoiceConsole;
    if DriverType = EDT_COUNT then
    begin
      ExitCode := 1;
      Exit;
    end;

    VideoDim.Init(800, 600);

    // create device and exit if creation failed
    Device := createDevice(DriverType, [640, 480], 16);
    if Device = nil then
    begin
      ExitCode := 1;   // could not create selected driver.
      Exit;
    end;

    if ParamCount > 1 then
      MapName := ParamStr(2)
    else
      MapName := QUAKE3_MAP_NAME;

    (*
    Get a pointer to the video driver and the SceneManager so that
    we do not always have to write device->getVideoDriver() and
    device->getSceneManager().
    *)
    Driver := Device.GetVideoDriver;
    Smgr := Device.GetSceneManager;
    Gui := Device.GetGUIEnvironment;

    //! add our private media directory to the file system
    Device.GetFileSystem.AddFileArchive('..\..\media\');

    (*
    To display the Quake 3 map, we first need to load it. Quake 3 maps
    are packed into .pk3 files, which are nothing other than .zip files.
    So we add the .pk3 file to our FileSystem. After it was added,
    we are able to read from the files in that archive as they would
    directly be stored on disk.
    *)
    if ParamCount > 2 then
      FileArchive := AnsiString(ParamStr(1))
    else
      FileArchive := QUAKE3_STORAGE_1;

    {$IFDEF ORIGINAL_QUAKE3_ARENA}
    Device.GetFileSystem.AddFolderFileArchive(PAnsiChar(FileArchive));
    {$ENDIF}
    {$IFDEF IRRLICHT_QUAKE3_ARENA}
    Device.GetFileSystem.AddFileArchive(PAnsiChar(FileArchive));
    {$ENDIF}

    {$IFDEF QUAKE3_STORAGE_2}
    Device.GetFileSystem.AddFolderFileArchive(PAnsiChar(AnsiString(QUAKE3_STORAGE_2)));
    {$ENDIF}

    // Quake3 Shader controls Z-Writing
    Smgr.GetParameters.SetAttribute(ALLOW_ZWRITE_ON_TRANSPARENT, True);

    (*
    Now we can load the mesh by calling getMesh(). We get a pointer returned
    to a IAnimatedMesh. As you know, Quake 3 maps are not really animated,
    they are only a huge chunk of static geometry with some materials
    attached. Hence the IAnimated mesh consists of only one frame,
    so we get the "first frame" of the "animation", which is our quake level
    and create an Octree scene node with it, using addOctreeSceneNode().
    The Octree optimizes the scene a little bit, trying to draw only geometry
    which is currently visible. An alternative to the Octree would be a
    AnimatedMeshSceneNode, which would draw always the complete geometry of
    the mesh, without optimization. Try it out: Write addAnimatedMeshSceneNode
    instead of addOctreeSceneNode and compare the primitives drawn by the
    video driver. (There is a getPrimitiveCountDrawed() method in the
    IVideoDriver class). Note that this optimization with the Octree is only
    useful when drawing huge meshes consisting of lots of geometry.
    *)
    Mesh := TQ3LevelMesh(Smgr.GetMesh(MapName));

    (*
      add the geometry mesh to the Scene ( polygon & patches )
      The Geometry mesh is optimised for faster drawing
    *)
    Node := nil;
    if Assigned(Mesh) then
    begin
      Geometry := Mesh.GetMesh(S32(E_Q3_MESH_GEOMETRY));
      Node := Smgr.AddOctreeSceneNode(Geometry, nil, -1, 4096);
    end;

    // create an event receiver for making screenshots
    screenshotFactory := TScreenShotFactory.Create(Device, MapName, Node);
    Device.SetEventReceiver(screenshotFactory);

    (*
      now construct SceneNodes for each Shader
      The Objects are stored in the quake mesh scene::E_Q3_MESH_ITEMS
      and the Shader ID is stored in the MaterialParameters
      mostly dark looking skulls and moving lava.. or green flashing tubes?
    *)
    if Assigned(Mesh) then
    begin
      // the additional mesh can be quite huge and is unoptimized
      AdditionalMesh := Mesh.GetMesh(S32(E_Q3_MESH_ITEMS));

      {$IFDEF SHOW_SHADER_NAME}
      Font := Device.GetGUIEnvironment.GetFont('..\..\media\fontlucida.png');
      Count := 0;
      {$ENDIF}

      for i := 0 to AdditionalMesh.GetMeshBufferCount - 1 do
      begin
        MeshBuffer := AdditionalMesh.GetMeshBuffer(i);
        MaterialPtr := MeshBuffer.GetMaterial;

        // The ShaderIndex is stored in the material parameter
        ShaderIndex := Round(MaterialPtr^.MaterialTypeParam2);

        // the meshbuffer can be rendered without additional support, or it has no shader
        ShaderPtr := Mesh.GetShader(ShaderIndex);
        if ShaderPtr = nil then
          continue;

        // we can dump the shader to the console in its
        // original but already parsed layout in a pretty
        // printers way.. commented out, because the console
        // would be full...
        // quake3::dumpShader ( Shader );
        Node := Smgr.AddQuake3SceneNode(MeshBuffer, ShaderPtr);

        {$IFDEF SHOW_SHADER_NAME}
        Inc(Count);
        ANodeName := WideString(AnsiString(Node.GetName));
        Smgr.AddBillboardTextSceneNode(Font, PWideChar(ANodeName),
          Node, Dimension2D(80, 8), Vector3DF(0, 10, 0));
        {$ENDIF}
      end;
    end;

    (*
    Now we only need a Camera to look at the Quake 3 map. And we want to
    create a user controlled camera. There are some different cameras
    available in the Irrlicht engine. For example the Maya Camera which can
    be controlled comparable to the camera in Maya: Rotate with left mouse
    button pressed, Zoom with both buttons pressed, translate with right
    mouse button pressed. This could be created with
    addCameraSceneNodeMaya(). But for this example, we want to create a
    camera which behaves like the ones in first person shooter games (FPS).
    *)
    Camera := Smgr.AddCameraSceneNodeFPS;

    (*
      so we need a good starting Position in the level.
      we can ask the Quake3 Loader for all entities with class_name
      "info_player_deathmatch"
      we choose a random launch
    *)
    if Assigned(Mesh) then
    begin
      EntityList := Mesh.GetEntityDataList;

      Search.Name := 'info_player_deathmatch';

      Index := EntityList.BinaryFind(@Search);

      if (Index >= 0) then
      begin
        repeat
          EntityPtr := PEntity(EntityList.Item[Index]);
          GroupPtr := EntityPtr^.GetGroup(1);

          ContextStr := GroupPtr^.Get('origin');
          DIrr.Quake3.StrToVector3DF(ContextStr, VPos);

          ContextStr := GroupPtr^.Get('angle');
          DIrr.Quake3.StrToF32(ContextStr, VAngle);

          VTarget.Init(0, 0, 1);
          VTarget.RotateXZBy(VAngle);

          Camera.SetPosition(VPos);
          Camera.SetTarget(VPos + VTarget);

          Inc(Index);

(*
				notEndList = (	index < (s32) entityList.size () &&
								entityList[index].name == search.name &&
								(device->getTimer()->getRealTime() >> 3 ) & 1
							);
*)

          if index = 2 then
            notEndList := 1
          else
            notEndList := 0;
        until (notEndList = 0);
      end;
    end;

    (*
    The mouse cursor needs not to be visible, so we make it invisible.
    *)
    Device.GetCursorControl.SetVisible(False);

    // load the engine logo
    Gui.AddImage(Driver.GetTexture('irrlichtlogo2.png'), Position2D(10, 10));

    // show the driver logo
    Pos2DI.Init(VideoDim.Width - 128, VideoDim.Height - 64);

    case DriverType of
      EDT_BURNINGSVIDEO: Gui.AddImage(Driver.GetTexture('burninglogo.png'), Pos2DI);
      EDT_OPENGL: Gui.AddImage(Driver.GetTexture('opengllogo.png'), Pos2DI);
      EDT_DIRECT3D8, EDT_DIRECT3D9:
        Gui.AddImage(Driver.GetTexture('directxlogo.png'), Pos2DI);
    end;

    (*
    We have done everything, so lets draw it. We also write the current
    frames per second and the drawn primitives to the caption of the
    window. The 'if (device->isWindowActive())' line is optional, but
    prevents the engine render to set the position of the mouse cursor
    after task switching when other program are active.
    *)

    while (Device.Run and Assigned(Driver)) do
    begin
      if Device.IsWindowActive then
      begin
        Driver.BeginScene(True, True, IrrColor(20, 20, 40, 255));
        Smgr.DrawAll;
        Gui.DrawAll;
        Driver.EndScene;

        FPS := Driver.GetFPS;
        Attr := Smgr.GetParameters;
        Str := 'Q3 [';
        Str := Str + Driver.GetName;
        Str := Str + '] FPS:';
        Str := Str + IntToStr(FPS);

        Device.SetWindowCaption(PChar(Str));
        LastFPS := FPS;

      end;
    end;

    Device.Drop;
    screenshotFactory.Free;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
